<?php
/**
 * @package Curly
 * @subpackage Enumeration
 * @version 0.9
 * @link http://curly.codeplex.com/
 * @license http://curly.codeplex.com/license The MIT License
 * @author Dawid Zawada
 */

namespace Curly\Enumeration
{   
    /**
     * Static utility class for working with enumerations
     * 
     * @package Curly
 	 * @subpackage Enumeration
 	 * @license http://curly.codeplex.com/license The MIT License
     */
    class EnumUtils
    {
        /**
         * Creates a new enumeration with the name given in the first parameter
         * and member names given in the second one
         * 
         * If you want the enumeration to be created in a specific namespace, include the namespace
         * in the $className param.
         * 
         * The new enumeration name must obey the rules for class names of the PHP language.
         * Member names of the new enumeration must obey the rules for method names of the PHP language.
         *  
         * @example Example of use:
         * <code>
         * use Enumeration\EnumUtils;
         * 
         * EnumUtils::createEnum( "Fruit", array( "Apple", "Orange" ) );
         * 
         * function isAnApple( Fruit $fruit )
         * {
         *     if( $fruit == Fruit::Apple() )
         *     {
         *         return "Yes, it is";
         *     }
         *     else
         *     {
         *         return "No, it isn't";
         *     }
         * }
         * 
         * echo isAnApple( Friut::Orange() );
         * </code>
         * 
         * @param string $className Enumeration name
         * @param array $names Array of member names
         * @throws \InvalidArgumentException
         */
        public static function createEnum( $className, array $names )
        {
            static::validateNewClassName( $className );

            $namespace = static::extractNamespace( $className );
            
            $code = "namespace {$namespace} { final class {$className} extends " . __NAMESPACE__ . "\\Enum {";
            foreach( $names as $name )
            {
                static::validateName( $name );
                $code .= "public static function {$name}() { return parent::___getInstance( __FUNCTION__ ); }";
            }
            $code .= "} }";

            eval( $code );
        }
        
        /**
         * Returns names of members of the given enumeration class
         * 
         * The class must exist and must inherit from the Enum class
         * 
         * @param string $className Enumeration name
         * @return array
         * @throws \InvalidArgumentException
         */
        public static function getNames( $className )
        {
            static::validateClassName( $className );
            
            $class = new \ReflectionClass( $className );
            $methods = $class->getMethods( \ReflectionMethod::IS_STATIC );
            $methods = array_filter( $methods, function( $method ) {
                return $method->isPublic();
            } );
            array_walk( $methods, function( &$method ) {
                $method = $method->name;
            } );
            
            return array_values( $methods );
        }
        
        /**
         * Returns an array of members of the given enumeration class
         * 
         * The class must exist and must inherit from the Enum class
         * 
         * @param string $className Enumeration name
         * @return array
         * @throws \InvalidArgumentException
         */
        public static function getValues( $className )
        {
            $methods = static::getNames( $className );
            $methods = array_flip( $methods );
            array_walk( $methods, function( &$item, $key ) {
                $item = $className::$key();
            } );
            
            return $methods;
        }
        
        /**
         * Determines whether the given enumeration class contains a member
         * with the specified name
         * 
         * The class must exist and must inherit from the Enum class
         * 
         * @param string $className Enumeration name
         * @param string $name Member name
         * @return bool
         * @throws \InvalidArgumentException
         */
        public static function isDefined( $className, $name )
        {
            static::validateClassName( $className );
            
            $class = new \ReflectionClass( $className );
            if( $class->hasMethod( $name ) )
            {
                $method = $class->getMethod( $name );
                return $method->isStatic() && $method->isPublic();
            }
            else
            {
                return false;
            }
        }
        
        /**
         * Returns the value of the member of the given enumeration class
         * 
         * The class must exist and must inherit from the Enum class.
         * The member must exist in the enumeration.
         * 
         * @param string $className Enumeration name
         * @param string $name Member name
         * @return Curly\Enumeration\Enum
         * @throws \InvalidArgumentException
         */
        public static function parse( $className, $name )
        {
            if( static::isDefined( $className, $name ) )
            {
                return $className::$name();
            }
            else
            {
                throw new \InvalidArgumentException( "Member '{$name}' not found in the {$className} enumeration" );
            }
        }
        
        /**
         * Returns the namespace part of the given class name
         * 
         * This method also subtracts the namespace part from the variable 
         * passed by a reference
         * 
         * @param string $className
         * @return string
         */
        protected static function extractNamespace( &$className )
        {
            $namespace = "";
            $lastNsSeparatorPos = strrpos( $className, "\\" );
            if( $lastNsSeparatorPos !== false )
            {
                $namespace = substr( $className, 0, $lastNsSeparatorPos );
                $className = substr( $className, $lastNsSeparatorPos + 1 );
                
                if( !empty( $namespace ) && $namespace[0] != "\\" )
                {
                    $namespace = "\\{$namespace}";
                }
            }
            
            return $namespace;
        }
        
        /**
         * Throws an exception if the given member name has incorrect format
         * 
         * Throws an exception if:
         * the name is empty or
         * the name does not obey the rules for method names of the PHP language or
         * the name starts with more than one underscore character
         * 
         * @internal
         * @param string $name Member name
         * @throws \InvalidArgumentException
         */
        public static function _validateName( $name )
        {
            if( empty( $name ) )
            {
                throw new \InvalidArgumentException( "Name of an enumeration member cannot be empty" );
            }
            if( !preg_match( "~^[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*$~", $name ) )
            {
                // the name doesn't obey the rules for method names of the PHP language
                throw new \InvalidArgumentException( "Name of an enumeration member contains illegal characters. The name must obey the rules for method names of the PHP language" );
            }
            if( strpos( $name, "__" ) === 0 )
            {
                // the name starts with more than one underscore character
                throw new \InvalidArgumentException( "Name of an enumeration member cannot start with more than one underscore character" );
            }
        }
        
        /**
         * Throws an exception if there is no enumeration with the given name
         * 
         * Throws an exception if:
         * the name is empty or
         * a class with the specified name doesn't exist or 
         * the class doesn't inherit from the Enum class
         * 
         * @param string $className Enumeration name
         * @throws \InvalidArgumentException
         */
        protected static function validateClassName( $className )
        {
            if( empty( $className ) )
            {
                throw new \InvalidArgumentException( "Class name cannot be empty" );
            }
            if( !class_exists( $className ) )
            {
                throw new \InvalidArgumentException( "Class '{$className}' does not exist" );
            }
            if( !is_subclass_of( $className, __NAMESPACE__ . "\\Enum" ) )
            {
                throw new \InvalidArgumentException( "Class '{$className}' must be a child of the '" . __NAMESPACE__ . "\\Enum' class" ); 
            }
        }
        
        /**
         * Throws an exception if there already is an enumeration with the given name
         * 
         * Throws an exception if:
         * the name is empty or
         * the name does not obey the rules for class names of the PHP language or 
         * a class with the specified name already exists
         * 
         * @param string $className Enumeration name
         * @throws \InvalidArgumentException
         */
        protected static function validateNewClassName( $className )
        {
            if( empty( $className ) )
            {
                throw new \InvalidArgumentException( "Class name cannot be empty" );
            }
            if( !preg_match( "~^[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*$~", $className ) )
            {
                // the name doesn't obey the rules for class names of the PHP language
                throw new \InvalidArgumentException( "Class name contains illegal characters. The name must obey the rules for class names of the PHP language" );
            }
            if( class_exists( $className ) )
            {
                throw new \InvalidArgumentException( "Class '{$className}' already exists" );
            }
        }
        
        /**
         * We don't want this class and its subclasses to be instantiated
         */
        private final function __construct() 
        {
            throw new \LogicException( "'EnumUtils' class cannot be instantiated" );
        }
    }
}